package org.dromara.common.excel.core;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.handler.WorkbookWriteHandler;
import com.alibaba.excel.write.handler.context.WorkbookWriteHandlerContext;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.excel.annotation.CellMerge;

import java.lang.reflect.Field;
import java.util.*;


/**
 * @Author 刘武贵
 * @Date 2024/8/15 22:00
 * @Description 功能描述：列值重复合并策略
 */
/**
 * CellMergeStrategy 类继承自 AbstractMergeStrategy 抽象类，
 * 并实现了 WorkbookWriteHandler 接口，用于在生成 Excel 工作簿时处理单元格合并策略。
 * 该类提供了具体的方法来在工作表中应用合并单元格的操作。
 */
@Slf4j
public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler {

    /**
     * 存储表格中单元格范围的列表
     */
    private final List<CellRangeAddress> cellList;

    /**
     * 标记表格是否有标题行
     */
    private final boolean hasTitle;

    /**
     * 当前行索引，用于迭代操作
     */
    private int rowIndex;

    /**
     * 构造函数，用于初始化单元格合并策略
     *
     * @param list 数据列表，包含需要合并的数据
     * @param hasTitle 布尔值，指示列表是否包含标题行
     */
    public CellMergeStrategy(List<?> list, boolean hasTitle) {
        // 初始化标题行是否存在
        this.hasTitle = hasTitle;
        // 行合并开始下标
        // 设置行合并的起始下标，如果存在标题行则从下标1开始，否则从下标0开始
        this.rowIndex = hasTitle ? 1 : 0;
        // 处理数据列表，根据是否包含标题行来决定合并策略
        this.cellList = handle(list, hasTitle);
    }

    /**
     * 根据单元格的写入情况，遍历合并区域，如果该单元格在区域内但不在第一行，则清空单元格内容
     * 解释：该方法的目的是为了在非首行的合并区域内清空单元格，以保持数据的一致性和整洁性。
     * 参数：
     * - sheet: 工作表对象
     * - cell: 当前单元格对象
     * - head: 表头对象（本例中未使用）
     * - relativeRowIndex: 相对于首行的行索引（本例中未使用）
     */
    @Override
    protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
        // 单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空
        // 获取当前单元格的行索引
        final int rowIndex = cell.getRowIndex();
        // 检查是否存在合并区域列表
        if (CollUtil.isNotEmpty(cellList)){
            // 遍历合并区域列表
            for (CellRangeAddress cellAddresses : cellList) {
                // 获取当前合并区域的首行索引
                final int firstRow = cellAddresses.getFirstRow();

                // 如果当前单元格在合并区域内且不在首行，则清空单元格内容
                if (cellAddresses.isInRange(cell) && rowIndex != firstRow){
                    cell.setBlank();
                }
            }
        }
    }

    /**
     * 在Workbook关闭后执行自定义操作
     * 此方法主要用于在Workbook关闭后，对表格进行额外的处理，如合并单元格
     *
     * @param context 表格写入上下文对象，通过它可以获取到正在写入的表格
     */
    @Override
    public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) {
        // 当前表格写完后，统一写入
        if (CollUtil.isNotEmpty(cellList)){
            // 遍历需要合并的单元格列表，逐个添加到表格中
            for (CellRangeAddress item : cellList) {
                context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item);
            }
        }
    }

    /**
     * 根据提供的数据列表和标题标志处理单元格合并
     *
     * @param list 数据列表，包含各种类型对象
     * @param hasTitle 表示列表是否包含标题行
     * @return 返回一个包含合并单元格范围的列表
     */
    @SneakyThrows
    private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
        // 初始化合并单元格范围列表
        List<CellRangeAddress> cellList = new ArrayList<>();
        // 如果数据列表为空，直接返回空的合并单元格范围列表
        if (CollUtil.isEmpty(list)) {
            return cellList;
        }

        // 获取列表中第一个对象的所有字段，排除serialVersionUID字段
        Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName()));

        // 有注解的字段
        // 用于存储有合并注解的字段和对应索引
        List<Field> mergeFields = new ArrayList<>();
        List<Integer> mergeFieldsIndex = new ArrayList<>();
        // 遍历所有字段，找出有CellMerge注解的字段
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            if (field.isAnnotationPresent(CellMerge.class)) {
                CellMerge cm = field.getAnnotation(CellMerge.class);
                mergeFields.add(field);
                // 如果注解中的索引为-1，则使用字段在数组中的索引
                mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
                // 如果有标题行，计算最大行索引
                if (hasTitle) {
                    ExcelProperty property = field.getAnnotation(ExcelProperty.class);
                    rowIndex = Math.max(rowIndex, property.value().length);
                }
            }
        }

        // 使用哈希图存储每个需要合并的字段及其重复单元格信息
        Map<Field, RepeatCell> map = new HashMap<>();
        // 遍历数据列表，生成两两合并单元格
        for (int i = 0; i < list.size(); i++) {
            for (int j = 0; j < mergeFields.size(); j++) {
                Field field = mergeFields.get(j);
                // 反射获取对象中字段的值
                Object val = ReflectUtils.invokeGetter(list.get(i), field.getName());

                int colNum = mergeFieldsIndex.get(j);
                // 如果map中没有此字段的重复单元格信息，则新增
                if (!map.containsKey(field)) {
                    map.put(field, new RepeatCell(val, i));
                } else {
                    RepeatCell repeatCell = map.get(field);
                    Object cellValue = repeatCell.getValue();
                    if (cellValue == null || "".equals(cellValue)) {
                        // 空值跳过不合并
                        continue;
                    }

                    // 如果当前值与前一个值不同，且满足合并条件，则添加合并单元格范围
                    if (!cellValue.equals(val)) {
                        if ((i - repeatCell.getCurrent() > 1) && isMerge(list, i, field)) {
                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
                        }
                        // 更新重复单元格信息
                        map.put(field, new RepeatCell(val, i));
                    } else if (i == list.size() - 1) {
                        // 如果是最后一个元素，且满足合并条件，则添加合并单元格范围
                        if (i > repeatCell.getCurrent() && isMerge(list, i, field)) {
                            cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
                        }
                    }
                }
            }
        }
        // 返回合并单元格范围列表
        return cellList;
    }

    /**
     * 判断当前项是否可以与前一项合并
     *
     * @param list 列表数据，包含待判断的项
     * @param i 当前项的索引
     * @param field 待判断合并的字段对象
     * @return true表示可以合并，false表示不可合并
     */
    private boolean isMerge(List<?> list, int i, Field field) {
        // 默认认为是可以合并的
        boolean isMerge = true;
        // 获取字段上CellMerge注解的信息
        CellMerge cm = field.getAnnotation(CellMerge.class);
        // 获取注解中指定的合并依据字段名数组
        final String[] mergeBy = cm.mergeBy();
        // 如果所有指定的合并依据字段名都不为空
        if (StrUtil.isAllNotBlank(mergeBy)) {
            // 比对当前list(i)和list(i - 1)的各个属性值一一比对 如果全为真 则为真
            // 遍历合并依据字段名数组
            for (String fieldName : mergeBy) {
                // 获取当前项（i）的指定字段值
                final Object valCurrent = ReflectUtil.getFieldValue(list.get(i), fieldName);
                // 获取前一项（i - 1）的指定字段值
                final Object valPre = ReflectUtil.getFieldValue(list.get(i - 1), fieldName);
                // 如果前一项和当前项的字段值不相等
                if (!Objects.equals(valPre, valCurrent)) {
                    // 依赖字段如有任一不等值,则标记为不可合并
                    isMerge = false;
                }
            }
        }
        // 返回合并判断结果
        return isMerge;
    }

    /**
     * RepeatCell类用于封装一个重复单元格的值及其重复次数
     * 该类主要用于表格数据处理，表示一个单元格值及其在行或列方向上的重复次数
     */
    @Data
    @AllArgsConstructor
    static class RepeatCell {

        /**
         * 单元格的值，可以是任意类型的对象
         */
        private Object value;

        /**
         * 当前单元格值的重复次数
         */
        private int current;

    }
}
